HTTP中的Transfer

您所在的位置:网站首页 content length是什么 HTTP中的Transfer

HTTP中的Transfer

2023-03-31 15:16| 来源: 网络整理| 查看: 265

1 Transfer-Encoding

        Transfer-Encoding,是一个 HTTP 头部字段,字面意思是「传输编码」。实际上,HTTP 协议中还有另外一个头部与编码有关:Content-Encoding(内容编码)。Content-Encoding 通常用于对实体内容进行压缩编码,目的是优化传输,例如用 gzip 压缩文本文件,能大幅减小体积。内容编码通常是选择性的,例如 jpg / png 这类文件一般不开启,因为图片格式已经是高度压缩过的,再压一遍没什么效果不说还浪费 CPU。

        而 Transfer-Encoding 则是用来改变报文格式,它不但不会减少实体内容传输大小,甚至还会使传输变大,那它的作用是什么呢?本文接下来主要就是讲这个。我们先记住一点,Content-Encoding 和 Transfer-Encoding 二者是相辅相成的,对于一个 HTTP 报文,很可能同时进行了内容编码和传输编码。

1.1 Persistent Connection

        暂时把 Transfer-Encoding 放一边,我们来看 HTTP 协议中另外一个重要概念:Persistent Connection(持久连接,通俗说法叫长连接)。我们知道,HTTP 是运行在传输层 TCP 连接协议之上的应用层协议,自然也有着跟 TCP 一样的三次握手、慢启动等特性,建立连接开销较大,为了尽可能的提高 HTTP 性能,使用持久连接就显得尤为重要了。为此,HTTP 协议引入了相应的机制。

        HTTP/1.0 的持久连接机制是后来才引入的,通过 Connection: keep-alive 这个头部来实现,服务端和客户端都可以使用它告诉对方在发送完数据之后不需要断开 TCP 连接,以备后用。HTTP/1.1 则规定所有连接都默认是持久的,除非显式地在头部加上 Connection: close。所以实际上,HTTP/1.1 中 Connection 这个头部字段已经没有 keep-alive 这个取值了,但由于历史原因,很多 Web Server 和浏览器,还是保留着给 HTTP/1.1 长连接发送 Connection: keep-alive 的习惯。

        浏览器重用已经打开的空闲持久连接,可以避开缓慢的三次握手,还可以避免遇上 TCP 慢启动的拥塞适应阶段,听起来十分美妙。

        为了深入研究持久连接的特性,我决定用 Node 写一个最简单的 Web Server 用于测试,Node 提供了 http 模块用于快速创建 HTTP Web Server,但我需要更多的控制,所以用 net 模块创建了一个 TCP Server:

require('net').createServer(function(sock) {       sock.on('data', function(data) {           sock.write('HTTP/1.1 200 OK\r\n');           sock.write('\r\n');           sock.write('hello world!');           sock.destroy();       });   }).listen(9090, '127.0.0.1');

        启动服务后,在浏览器里访问 127.0.0.1:9090,正确输出了指定内容,一切正常。去掉 sock.destroy() 这一行,让它变成持久连接,重启服务后再访问一下。这次的结果就有点奇怪了:迟迟看不到输出,通过 Network 查看请求状态,一直是 pending。

        这是因为,对于非持久连接,浏览器可以通过连接是否关闭来界定请求或响应实体的边界;而对于持久连接,这种方法显然不奏效。上例中,尽管我已经发送完所有数据,但浏览器并不知道这一点,它无法得知这个打开的连接上是否还会有新数据进来,只能傻傻地等了。

1.2 Content-Length

        要解决上面这个问题,最容易想到的办法就是计算实体长度,并通过头部告诉对方。这就要用到 Content-Length 了,改造一下上面的例子:

require('net').createServer(function(sock) {       sock.on('data', function(data) {           sock.write('HTTP/1.1 200 OK\r\n');           sock.write('Content-Length: 12\r\n');           sock.write('\r\n');           sock.write('hello world!');       });   }).listen(9090, '127.0.0.1');

        这次我们使用了另一个头部字段:Content-Length。可以看到,这次发送完数据并没有关闭 TCP 连接,但浏览器能正常输出内容并结束请求,因为浏览器可以通过 Content-Length 的长度信息,判断出响应实体已结束。那如果 Content-Length 和实体实际长度不一致会怎样?有兴趣的同学可以自己试试,通常如果 Content-Length 比实际长度短,会造成内容被截断;如果比实体内容长,会造成 pending。

        由于 Content-Length 字段必须真实反映实体长度,但实际应用中,有些时候实体长度并没那么好获得,例如实体来自于网络文件,或者由动态语言生成。这时候要想准确获取长度,只能在服务端开一个足够大的 buffer,等内容全部生成好再计算Content-Length,然后才将内容发送给客户端。但这样做一方面需要更大的内存开销,另一方面也会让客户端等更久。

        我们在做 WEB 性能优化时,有一个重要的指标叫 TTFB(Time To First Byte),它代表的是从客户端发出请求到收到响应的第一个字节所花费的时间。大部分浏览器自带的 Network 面板都可以看到这个指标,越短的 TTFB 意味着用户可以越早看到页面内容,体验越好。可想而知,服务端为了计算响应实体长度而缓存所有内容,跟更短的 TTFB 理念背道而驰。但在 HTTP 报文中,实体一定要在头部之后,顺序不能颠倒,为此我们需要一个新的机制:不依赖头部的长度信息,也能知道实体的边界。

1.3 Transfer-Encoding: chunked

        本文主角终于再次出现了,Transfer-Encoding 正是用来解决上面这个问题的。历史上 Transfer-Encoding 可以有多种取值,为此还引入了一个名为 TE 的头部用来协商采用何种传输编码。但是最新的 HTTP 规范里,只定义了一种传输编码:分块编码(chunked)。

        分块编码相当简单,在头部加入 Transfer-Encoding: chunked 之后,就代表这个报文采用了分块编码。这时,报文中的实体需要改为用一系列分块来传输。每个分块包含十六进制的长度值和数据,长度值独占一行,长度不包括它结尾的 CRLF(\r\n),也不包括分块数据结尾的 CRLF。最后一个分块长度值必须为 0,对应的分块数据没有内容,表示实体结束。按照这个格式改造下之前的代码:

require('net').createServer(function(sock) {       sock.on('data', function(data) {           sock.write('HTTP/1.1 200 OK\r\n');           sock.write('Transfer-Encoding: chunked\r\n');           sock.write('\r\n');               sock.write('b\r\n');           sock.write('01234567890\r\n');               sock.write('5\r\n');           sock.write('12345\r\n');               sock.write('0\r\n');           sock.write('\r\n');       });   }).listen(9090, '127.0.0.1');

        上面这个例子中,我在响应头中表明接下来的实体会采用分块编码,然后输出了 11 字节的分块,接着又输出了 5 字节的分块,最后用一个 0 长度的分块表明数据已经传完了。用浏览器访问这个服务,可以得到正确结果。可以看到,通过这种简单的分块策略,很好的解决了前面提出的问题。

2 Transfer-Encoding: chunked 与 Content-Length

        Transfer-Encoding: chunked 与 Content-Length 同为头部字段,它们不会同时出现在头部中,当使用分块传输时,头部将出现 Transfer-Encoding: chunked,而不再包含Content-Length字段,即使强行设定该字段,也会被忽略。

        在HTTP中,我们通常依赖 HttpCode/HttpStatus 来判断一个 HTTP 请求是否成功,如:

HTTP: Status 200 – 成功,服务器成功返回网页

HTTP: Status 304 – 成功,网页未修改

HTTP: Status 404 – 失败,请求的网页不存在

HTTP: Status 503 – 失败,服务不可用

… …

        但开发人员有时候也会有令人意外的想象力。我们的一部分开发人员决定使用 Content-Length 来判断 HTTP 请求是否成功,当 Content-Length 的值小于等于0或者为162时,认为请求失败。

        当 Content-Length 的值小于等于0时认为http请求失败还好理解,因为开发人员错误地以为 HTTP 响应头中一定会包含 Content-Length 字段。

        为什么当 Content-Length 的值为162时,也认为请求失败呢。这是因为我们的服务器的404页面的长度恰好是162。惊不惊喜,意不意外!

        以上开发的代码长期以来并没有出现什么问题,直到有一天我在服务端 Nginx 开启了chunked_transfer_encoding。此时,HTTP 使用了分块传输模式,HTTP 响应头中出现了 Transfer-Encoding: chunked,而不再包含 Content-Length,开发的代码始终取到 Content-Length 的值为-1,一直认为下载失败,导致客户端无法正常运行。

        当然,我只要稍微修改404页面,也会导致开发的下载判断出现问题。更何况,或许下载资源的长度刚好就是162呢?

2.1 gzip 与 Transfer-Encoding: chunked       gzip on;           gzip_min_length 1k;           gzip_buffers 4 16k;           #gzip_http_version 1.0;           gzip_comp_level 2;           gzip_types text/xml text/plain text/css text/js application/javascript application/json;           gzip_vary on;           gzip_disable "MSIE [1-6]\.";

        当在 Nginx 的配置文件 nginx.conf 的location等位置配置以上内容时,Nginx 服务器将对指定的文件类型开启压缩 (gzip)以优化传输,减少传输量。

        分块传输可以将压缩对象分为多个部分,在这种情况下,资源整个进行压缩,压缩的输出分块传输。在压缩的情形中,分块传输有利于一边进行压缩一边发送数据,而不是先完成压缩过程,得知压缩后数据的大小之后再进行传输,从而使得用户能够更快地接受到数据,TTFB 指标更好。

        对于开启了 gzip 的传输,报文的头部将增加 Content-Encoding: gzip 来标记传输内容的编码方式。同时,Nginx 服务器默认就会对压缩内容进行分块传输,而无须显示开启chunked_transfer_encoding。

        Nginx 中如何关闭分块传输呢,在 Nginx 配置文件 location 段中加一行“chunked_transfer_encoding off;”即可。

location / {         chunked_transfer_encoding       off; }

 



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3